﻿// (c) Copyright 2013 Mike Riedel.
// This source is subject to the Microsoft Public License (Ms-PL).
// All other rights reserved.

using System;
using System.Collections.Generic;
using System.Linq;

namespace FreshMeat.Storage
{
   /// <summary>
   /// Customizable implementation of <see cref="IRepository{TId,TItem}"/>.
   /// </summary>
   /// <typeparam name="TId"></typeparam>
   /// <typeparam name="TItem"></typeparam>
   /// <typeparam name="TData"></typeparam>
   public class Repository<TId, TItem, TData> : IRepository<TId, TItem>
   {
      readonly IMapper<TId, TItem, TData> mapper;
      readonly IStore<TId, TData> store;

      /// <summary>
      /// Creates instance using specified parameters.
      /// </summary>
      /// <param name="mapper">Mapper to be used.</param>
      /// <param name="store">Store to be used.</param>
      public Repository(IMapper<TId, TItem, TData> mapper, IStore<TId, TData> store)
      {
         this.mapper = mapper;
         this.store = store;
      }

      /// <summary>
      /// Saves an item.
      /// If an item with the same ID already exists, it is ovewritten; otherwise, the item is created.
      /// The item is not modified in any way.
      /// The caller is free to modify the item after saving; doing so will not affect the saved item.
      /// </summary>
      /// <param name="item"></param>
      /// <returns>True if the item already existed, false if it was created.</returns>
      public bool Save(TItem item)
      {
         var temp = PreSave(item);

         using (var context = store.CreateContext())
            return context.Save(temp.Id, temp.Data);
      }

      /// <summary>
      /// Saves zero or more items.
      /// Everything written in <see cref="IRepository{TId,TItem}.Save(TItem)"/> applies to each item here.
      /// For multiple items this is faster (and more convenient) than saving items one at a time.
      /// </summary>
      /// <param name="items">Items to save.</param>
      /// <returns>The number of items that already existed or 0 if all items were just created.
      /// Note that if <paramref name="items"/> is empty then this value will also be 0.</returns>
      public int Save(IEnumerable<TItem> items)
      {
         var temps = items.Select(PreSave);

         using (var context = store.CreateContext())
            return temps.Select(temp => context.Save(temp.Id, temp.Data)).Count(updated => updated);
      }

      TempResult PreSave(TItem item)
      {
         return new TempResult
         {
            Id = mapper.ToId(item),
            Data = mapper.ToData(item)
         };
      }

      /// <summary>
      /// Attempts to load an item.
      /// </summary>
      /// <param name="id">The item's ID.</param>
      /// <param name="factory">Optional function to supply a value to be returned if the item doesn't 
      /// exist; if this is null then <see cref="NotFoundException"/> is thrown if the item doesn't exist.</param>
      /// <returns>The item that was loaded or the value returned by the factory function.</returns>
      public TItem Load(TId id, Func<TId, TItem> factory = null)
      {
         return Load(new [] {id}, factory).First();
      }

      /// <summary>
      /// Attempts to load zero or more items.
      /// Everything written in <see cref="IRepository{TId,TItem}.Load(TId,System.Func{TId,TItem})"/> applies to each item here.
      /// For multiple items this is faster (and more convenient) than loading items one at a time.
      /// </summary>
      /// <param name="ids">ID's of items to load.</param>
      /// <param name="factory">Optional function to supply a value to be returned if the item doesn't 
      /// exist; if this is null then <see cref="NotFoundException"/> is thrown if the item doesn't exist.</param>
      /// <returns>Items that were loaded and/or the values returned by the factory function.</returns>
      public IEnumerable<TItem> Load(IEnumerable<TId> ids, Func<TId, TItem> factory = null)
      {
         TData[] datas;
         using (var context = store.CreateContext())
            datas = ids.Select(id => InternalLoad(context, id, factory)).ToArray(); // No deferred execution within lock

         return datas.Select(mapper.ToItem); // Deferred execution ok here
      }

      TData InternalLoad(IStoreContext<TId, TData> context, TId id, Func<TId, TItem> factory)
      {
         TData data;
         if (context.Load(id, out data))
            return data;

         if (factory == null)
            throw new NotFoundException {Id = id};

         var item = factory(id);
         return mapper.ToData(item);
      }

      /// <summary>
      /// Loads all items.
      /// </summary>
      /// <returns>Items that were loaded; this may be empty if no items exist.</returns>
      public IEnumerable<TItem> Load()
      {
         TData[] temps;
         using (var context = store.CreateContext())
            temps = context.LoadMany().ToArray(); // No deferred execution within lock

         return temps.Select(mapper.ToItem); // Deferred execution ok here
      }

      /// <summary>
      /// Attempts to delete an item.
      /// </summary>
      /// <param name="id">The item's ID.</param>
      /// <returns>True if the item was deleted; false if it did not exist.</returns>
      public bool Delete(TId id)
      {
         using (var context = store.CreateContext())
            return context.Delete(id);
      }

      /// <summary>
      /// Attempts to delete zero or more items.
      /// </summary>
      /// <param name="ids">ID's of items to delete.</param>
      /// <returns>The number of items that were deleted.</returns>
      public int Delete(IEnumerable<TId> ids)
      {
         using (var context = store.CreateContext())
            return ids.Select(context.Delete).Count(deleted => deleted);
      }

      struct TempResult
      {
         public TId Id;
         public TData Data;
      }
   }
}